#![allow(dead_code)]
use std::{collections::HashSet, net::IpAddr};

use tracing::{debug, trace, warn};

use bc_util::time_provider::TimeProvider;

#[derive(Debug)]
pub struct TimeConsensus<T: TimeProvider> {
    time_offset: i64,
    node_addr: HashSet<IpAddr>,
    time_offset_seq: Vec<i64>,
    time_provider: T,
}

impl<T: TimeProvider> TimeConsensus<T> {
    pub fn new(time_provider: T) -> Self {
        let time_offset_seq = vec![0];
        Self {
            time_offset: 0,
            node_addr: HashSet::new(),
            time_offset_seq,
            time_provider,
        }
    }

    pub fn get_adjusted_timestamp(&self) -> u64 {
        let cur_time = self.time_provider.current_time();
        trace!("cur_time: {}, time_offset: {}", cur_time, self.time_offset);
        u64::try_from(cur_time + self.time_offset).unwrap()
    }

    pub fn add_time_data(&mut self, ip: IpAddr, time: u64) {
        let time_offset = i64::try_from(time).unwrap() - self.time_provider.current_time();

        if !self.node_addr.insert(ip) {
            return;
        };
        self.time_offset_seq.push(time_offset);

        let seq_len = self.time_offset_seq.len();
        debug!(
            "Add time_offset ({} seconds, {} minutes) to time_offset_seq (cnt: {})",
            self.time_offset,
            self.time_offset / 60,
            seq_len
        );

        if seq_len >= 5 && (seq_len & 1) == 1 {
            self.time_offset_seq.sort();
            let median = self.time_offset_seq[seq_len / 2];
            self.update_time_offset(median);
            debug!(
                "Update time_offset to {}, {} minutes",
                self.time_offset,
                self.time_offset / 60
            );
            debug!("time_offset_seq: {:?}", self.time_offset_seq)
        }
    }

    fn update_time_offset(&mut self, median_val: i64) {
        if median_val.abs() > 5 * 60 {
            let ntp_time = self.time_provider.ntp_time();
            if ntp_time.is_some() {
                self.time_offset = ntp_time.unwrap() - self.time_provider.current_time();
            } else {
                warn!("Failed to get time from NTP server");
            }
        } else {
            self.time_offset = median_val;
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use std::str::FromStr;

    #[derive(Debug, Clone)]
    struct MockTimeProvider {
        mock_time: i64,
    }

    impl MockTimeProvider {
        pub fn new(time: i64) -> Self {
            Self { mock_time: time }
        }
    }

    impl TimeProvider for MockTimeProvider {
        fn current_time(&self) -> i64 {
            self.mock_time
        }
        fn ntp_time(&self) -> Option<i64> {
            todo!()
        }
    }

    const TEST_TIME: i64 = 1620000000;

    #[test]
    fn test_new_initialization() {
        let provider = MockTimeProvider::new(TEST_TIME);
        let tc = TimeConsensus::new(provider);
        assert_eq!(tc.time_offset, 0);
        assert!(tc.node_addr.is_empty());
        assert_eq!(tc.time_offset_seq, vec![0]);
    }

    #[test]
    fn test_get_adjusted_timestamp() {
        let provider = MockTimeProvider::new(TEST_TIME);
        let mut tc = TimeConsensus::new(provider.clone());
        tc.time_offset = 300;

        assert_eq!(
            tc.get_adjusted_timestamp(),
            provider.current_time() as u64 + 300
        );
    }

    #[test]
    fn test_duplicate_ip_handling() {
        let provider = MockTimeProvider::new(TEST_TIME);
        let mut tc = TimeConsensus::new(provider.clone());
        let ip = IpAddr::from_str("192.168.1.1").unwrap();

        let cur_time = provider.current_time() as u64;
        tc.add_time_data(ip, cur_time + 100);
        tc.add_time_data(ip, cur_time + 200);

        assert_eq!(tc.time_offset_seq.len(), 2);
        assert_eq!(tc.time_offset_seq[0], 0);
        assert_eq!(tc.time_offset_seq[1], 100);
    }

    #[test]
    fn test_median_calculation() {
        let provider = MockTimeProvider::new(TEST_TIME);
        let mut tc = TimeConsensus::new(provider.clone());
        let test_ips = [
            "192.168.1.1",
            "192.168.1.2",
            "192.168.1.3",
            "192.168.1.4",
            "192.168.1.5",
        ];

        // [5, 3, 1, -1, -3]
        for (i, &ip) in test_ips.iter().enumerate() {
            let ip = IpAddr::from_str(ip).unwrap();
            let mut time = 5 - 2 * i as i64;
            let cur_time = provider.current_time();
            time += cur_time;
            tc.add_time_data(ip, time as u64);
        }

        assert_eq!(tc.time_offset_seq, vec![-1, 0, 1, 3, 5, -3]);
        assert_eq!(tc.time_offset, 1);
    }

    #[test]
    #[should_panic(expected = "not yet implemented")]
    fn test_ntp_threshold_handling() {
        let provider = MockTimeProvider::new(TEST_TIME);
        let mut tc = TimeConsensus::new(provider);

        tc.update_time_offset(301);
    }

    #[test]
    fn test_add_time_data() {
        let provider = MockTimeProvider::new(TEST_TIME);
        let mut tc = TimeConsensus::new(provider.clone());
        let current_time = provider.current_time() as u64;

        let ip = IpAddr::from_str("192.168.1.1").unwrap();
        tc.add_time_data(ip, current_time + 10);

        let ip = IpAddr::from_str("192.168.1.2").unwrap();
        tc.add_time_data(ip, current_time - 10);

        assert_eq!(tc.time_offset_seq.len(), 3);
        assert_eq!(tc.time_offset_seq[0], 0);
        assert_eq!(tc.time_offset_seq[1], 10);
        assert_eq!(tc.time_offset_seq[2], -10);
    }
}
